/*
* Copyright (C) 2012-2014 qar
* License: http://www.gnu.org/licenses/gpl.html GPL version 2
*/

#include "extract.h"

#include <sys/time.h>
#include <unistd.h>

#include "crypto/encrypteddevice.h"
#include "outputstream.h"
#include "util.h"

// Extracts a directory from the archive.
void extractDir(QDataStream &in, qint32 ver, QString rootPath, QDir &rootDir,
                qint64 startTime, qint64 &lastProgressTime,
                qint64 &lastProgressBytes, qint64 &progressBytes)
{
    QString relativeDirPath;
    // Read the attributes
    qint32 perm;
    QDateTime tsCreated;
    QDateTime tsModified;
    QDateTime tsRead;
    in >> relativeDirPath >> perm >> tsCreated >> tsModified >> tsRead;
    quint32 owner;
    quint32 group;
    if (ver >= 2) {
        in >> owner >> group;
    }
    // Compute the canonical path (rootPath + / + relativeDirPath)
    QString dirPath = rootPath +
                      (relativeDirPath.startsWith(QDir::separator()) ?
                           relativeDirPath :
                           (QDir::separator() + relativeDirPath));
    // Create the directory
    if (!rootDir.mkpath(dirPath)) {
        qErr() << "\nError: Could not create dir" << dirPath;
        exit(-1);
    }
    // Set the attributes
    if (!QFile::setPermissions(dirPath, QFile::Permissions(perm))) {
        qErr() << "\nWarning: Could not set permissions for dir" << dirPath;
    }
    if (ver >= 2) {
		if (chown(dirPath.toLatin1().constData(), owner, group) != 0) {
            // Only root can change it, so this will always fail for regular users, stay silent
            // qErr() << "\nWarning: Could not set owner/group for dir" << dirPath;
        }
    }
	struct timeval timestamps[2];
	timestamps[0].tv_usec = 0;
	timestamps[0].tv_sec = tsRead.toMSecsSinceEpoch() / 1000;
	timestamps[1].tv_usec = 0;
	timestamps[1].tv_sec = tsModified.toMSecsSinceEpoch() / 1000;
	if (utimes(dirPath.toLatin1().constData(), timestamps) != 0) {
		qErr() << "\nWarning: Could not set timestamp for dir" << dirPath;
	}
    displayProgress(relativeDirPath, progressBytes,
                    startTime, lastProgressTime, lastProgressBytes);
}

// Extracts a file from the archive.
void extractFile(QDataStream &in, qint32 ver, QString rootPath,
                 qint64 startTime, qint64 &lastProgressTime,
                 qint64 &lastProgressBytes, qint64 &progressBytes)
{
    QString relativeFilePath;
    // Read attributes
    qint32 perm;
    QDateTime tsCreated;
    QDateTime tsModified;
    QDateTime tsRead;
    in >> relativeFilePath >> perm >> tsCreated >> tsModified >> tsRead;
    quint32 owner;
    quint32 group;
    if (ver >= 2) {
        in >> owner >> group;
    }
    // Compute the canonical path (rootPath + / + relativeFilePath)
    QString filePath = rootPath +
                       (relativeFilePath.startsWith(QDir::separator()) ?
                            relativeFilePath :
                            (QDir::separator() + relativeFilePath));
    // Create the file
	{
		QFile file(filePath);
		if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
			qErr() << "\nError: Failed to create file:" << filePath;
			exit(-1);
		}
		displayProgress(relativeFilePath, progressBytes,
						startTime, lastProgressTime, lastProgressBytes);
		// Write the file contents
		while (true) {
			QChar chunkMarker('0');
			in >> chunkMarker;
			if (chunkMarker == 'c' || chunkMarker == 'z') {
				// Data chunk
				QByteArray data;
				in >> data;
				if (chunkMarker == 'z') {
					data = qUncompress(data);
				}
				while (!data.isEmpty()) {
					qint64 writeCount = file.write(data);
					if (writeCount <= 0) {
						qErr() << "\nError: Write error for file" << filePath;
						exit(-1);
					}
					data = data.mid(writeCount);
					progressBytes += writeCount;
					displayProgress(relativeFilePath, progressBytes,
									startTime, lastProgressTime, lastProgressBytes);
				}
			} else if (chunkMarker == 'e') {
				// End of file
				break;
			} else {
				// Error
				qErr() << "\nError: Unknown chunk marker (archive corrupted?)" << chunkMarker;
				exit(-1);
			}
		}
		file.close();
	}

    // Set the permissions
    if (!QFile::setPermissions(filePath, QFile::Permissions(perm))) {
        qErr() << "\nWarning: Could not set permissions for file" << filePath;
    }
    if (ver >= 2) {
		if (chown(filePath.toLatin1().constData(), owner, group) != 0) {
            // Only root can change it, so this will always fail for regular users, stay silent
            // qErr() << "\nWarning: Could not set owner/group for file" << filePath;
        }
    }
	struct timeval timestamps[2];
	timestamps[0].tv_usec = 0;
	timestamps[0].tv_sec = tsRead.toMSecsSinceEpoch() / 1000;
	timestamps[1].tv_usec = 0;
	timestamps[1].tv_sec = tsModified.toMSecsSinceEpoch() / 1000;
	if (utimes(filePath.toLatin1().constData(), timestamps) != 0) {
		perror("");
		qErr() << "\nWarning: Could not set timestamp for file" << filePath;
	}
}

// Extracts a symlink from the archive.
void extractSymlink(QDataStream &in, qint32 ver, QString rootPath,
                    qint64 startTime, qint64 &lastProgressTime,
                    qint64 &lastProgressBytes, qint64 &progressBytes)
{
    Q_UNUSED(ver);
    QString relativeFilePath;
    QString target;
    bool relativeToRoot = false;
    // Read attributes
    if (ver >= 4) {
        in >> relativeToRoot >> relativeFilePath >> target;
    } else {
        relativeToRoot = true;
        in >> relativeFilePath >> target;
    }
    if (!target.startsWith(QDir::separator())) {
        if (relativeToRoot) {
            target = rootPath + QDir::separator() + target;
        }
    }
    // Compute the canonical path (rootPath + / + relativeFilePath)
    QString filePath = rootPath +
                       (relativeFilePath.startsWith(QDir::separator()) ?
                            relativeFilePath :
                            (QDir::separator() + relativeFilePath));
    QString oldCd = QDir::currentPath();
    QDir::setCurrent(getParentDir(filePath));
    if (!QFile::link(target, filePath)) {
        if (!QFile::exists(filePath)) {
            qErr() << "\nWarning: Could not create symlink" << filePath << "->" << target;
        }
    }
    QDir::setCurrent(oldCd);
    displayProgress(relativeFilePath, progressBytes,
                    startTime, lastProgressTime, lastProgressBytes);
}

void extract(QString archiveName, QString decryptionKeyFile)
{
	QString rootPath = QDir().absolutePath() + "/" + QFileInfo(archiveName).fileName().replace(QRegExp("\\.qar$"), "");
    qErr() << QString("Extracting %1 to %2 ...").arg(archiveName).arg(rootPath);

    // Do not overwrite.
    if (QFile(rootPath).exists()) {
        qErr() << "\nError: Dir already exists, aborting:" << rootPath;
        exit(-1);
    }

    // Create the root dir
    QDir rootDir;
    if (!rootDir.mkpath(rootPath)) {
        qErr() << "\nError: Could not create dir" << rootPath;
        exit(-1);
    }

    // Open the archive file for reading
    QFile archiveFile(archiveName);
    if (!archiveFile.open(QIODevice::ReadOnly)) {
        qErr() << "\nFailed to read file:" << archiveName;
        exit(-1);
    }

    EncryptedDevice encryptedFile(&archiveFile);

    QDataStream in;
    in.setVersion(QDataStream::Qt_4_0);

    QDir::setCurrent(rootPath);

    in.setDevice(&archiveFile);

    // Read the version number of the contents
    qint32 ver;
    in >> ver;

    qErr() << "Archive version:" << ver;

    if (ver > 4) {
        qWarn() << "Cannot read archive, unknown version:" << ver;
        exit(-1);
    }

    if (ver >= 3) {
		encryptedFile.open(QIODevice::ReadOnly,
						   Hasher::Sha1,
						   Cipher::NoEncryption,
						   Authenticator::NoAuthentication,
						   decryptionKeyFile);
        in.setDevice(&encryptedFile);
    }

    qint64 startTime = currentMSecsSinceEpoch();
    qint64 lastProgressTime = startTime;
    qint64 lastProgressBytes = 0;
    qint64 progressBytes = 0;

    while (!in.atEnd()) {
        QChar fileType('0');
        in >> fileType;
        // We are reading an entry of type...
        if (fileType == 'd') {
            // This is a subdirectory
            extractDir(in, ver, rootPath, rootDir,
                       startTime, lastProgressTime,
                       lastProgressBytes, progressBytes);
        } else if (fileType == 'f') {
            // This is a file
            extractFile(in, ver, rootPath,
                        startTime, lastProgressTime,
                        lastProgressBytes, progressBytes);
        } else if (fileType == 's' && ver >= 2) {
            // This is a symlink
            extractSymlink(in, ver, rootPath,
                           startTime, lastProgressTime,
                           lastProgressBytes, progressBytes);
        } else {
            // Error
            qErr() << "\nUnknown fileType" << fileType;
            exit(-1);
        }
    }
    if (in.status() != QDataStream::Ok) {
        qErr() << "\nRead error (archive corrupt?)";
        exit(-1);
    }
    if (ver >= 3) {
        encryptedFile.close();
    }
    qErr() << "\nFinished extracting" << archiveName;
}
